#include "MeshProg.h"
#include "camera.h"
#include <QOpenGLContext>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLFunctions_3_3_Core>
#include <QImage>
#include <QOpenGLTexture>
#include <assimp/scene.h>
#include <assimp/mesh.h>
#include <assimp/material.h>


MeshProg::MeshProg(QObject *parent)
    : QObject(parent)
    , ctx_(nullptr)
    , glFunc_(nullptr)
    , prog_(nullptr)
    , vao_(nullptr)
    , vbo_(nullptr)
    , vboNormal_(nullptr)
    , vboTexture_(nullptr)
    , ebo_(nullptr)
    , diffuseMap_(nullptr)
    , specularMap_(nullptr)
    , mesh_(nullptr)
    , numIndices_(0)
    , vertexIndices_(nullptr)
    , modelLoc_(-1)
    , viewLoc_(-1)
    , projectionLoc_(-1)
    , viewPosLoc_(-1)
{
    material_.ambient = QVector3D(1.0f, 0.5f, 0.31f);
    material_.diffuse = QVector3D(1.0f, 0.5f, 0.31f);
    material_.specular = QVector3D(1.0f, 0.5f, 0.31f);
    material_.shininess = 32.0f;
}

MeshProg::~MeshProg()
{
    reset();
}

bool MeshProg::initialize(QOpenGLContext *ctx, const aiMesh* mesh, const aiScene *scene, const QDir &dir)
{
    if (! (ctx && mesh)) {
        qDebug("%s: this %p, ctx %p, mesh %p", Q_FUNC_INFO, this, ctx, mesh);
        return false;
    }

    if (! (mesh->mNumVertices && mesh->mVertices)) {
        qDebug("%s: this %p, numVertices %d, vertices %p", Q_FUNC_INFO, this, mesh->mNumVertices, mesh->mVertices);
        return false;
    }

    if (! (mesh->mNumFaces && mesh->mFaces)) {
        qDebug("%s: this %p, numFaces %d, faces %p", Q_FUNC_INFO, this, mesh->mNumFaces, mesh->mFaces);
        return false;
    }

    ctx_ = ctx;
    mesh_ = mesh;

    connect(ctx_, SIGNAL(aboutToBeDestroyed()), SLOT(reset()));
    glFunc_ = ctx->versionFunctions<QOpenGLFunctions_3_3_Core>();

    // create vertex buffer objects (VBO)
    vbo_ = new QOpenGLBuffer;
    bool rc = vbo_->create();
    Q_ASSERT(rc);

    rc = vbo_->bind();
    Q_ASSERT(rc);

    //vbo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    vbo_->allocate(mesh_->mVertices, static_cast<int>(sizeof(aiVector3D) * mesh_->mNumVertices));

    // normals
    if (mesh_->mNormals) {
        vboNormal_ = new QOpenGLBuffer;
        rc = vboNormal_->create();
        Q_ASSERT(rc);

        rc = vboNormal_->bind();
        Q_ASSERT(rc);

        //vboNormal_->setUsagePattern(QOpenGLBuffer::StaticDraw);
        vboNormal_->allocate(mesh_->mNormals, static_cast<int>(sizeof(aiVector3D) * mesh_->mNumVertices));
    }

    // texture
    const aiMaterial* mat = scene->mMaterials[mesh_->mMaterialIndex];

    if (mat->GetTextureCount(aiTextureType_DIFFUSE)) {
        aiString str;
        mat->GetTexture(aiTextureType_DIFFUSE, 0, &str);
        QString textureFilePath = dir.absoluteFilePath(str.C_Str());

        if (QFile::exists(textureFilePath)) {
            material_.diffuseImg = QImage(textureFilePath);

            if ((! material_.diffuseImg.isNull()) && mesh_->mTextureCoords[0]) {
                material_.diffuseImg = material_.diffuseImg.mirrored();
                diffuseMap_ = new QOpenGLTexture(material_.diffuseImg);
                diffuseMap_->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
                diffuseMap_->setMagnificationFilter(QOpenGLTexture::Linear);

                vboTexture_ = new QOpenGLBuffer;
                rc = vboTexture_->create();
                Q_ASSERT(rc);

                rc = vboTexture_->bind();
                Q_ASSERT(rc);

                // vboTexture_->setUsagePattern(QOpenGLBuffer::StaticDraw);
                vboTexture_->allocate(mesh_->mTextureCoords[0], static_cast<int>(sizeof(aiVector3D) * mesh_->mNumVertices));
            }
        }
    }

    vbo_->bind();

    // create VAO
    rc = false;
    vao_ = new QOpenGLVertexArrayObject(this);
    rc = vao_->create();
    Q_ASSERT(rc);

    vao_->bind();

    // create element buffer objects (EBO)
    ebo_ = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
    rc = ebo_->create();
    Q_ASSERT(rc);

    rc = ebo_->bind();
    Q_ASSERT(rc);

    //ebo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexIndices_ = initializeVertexIndices(numIndices_);

    if (! vertexIndices_) {
        reset();
        return false;
    }

    ebo_->allocate(vertexIndices_, static_cast<int>(numIndices_ * sizeof(unsigned int)));

    // create program
    prog_ = new QOpenGLShaderProgram(this);

    rc = prog_->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/TextureMesh.vert");
    Q_ASSERT(rc);

    rc = prog_->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/TextureMesh.frag");
    Q_ASSERT(rc);

    rc = prog_->link();
    Q_ASSERT(rc);

    rc = prog_->bind();
    Q_ASSERT(rc);

    prog_->enableAttributeArray(0);
    prog_->setAttributeBuffer(0, GL_FLOAT, 0, 3);

    if (vboNormal_) {
        vboNormal_->bind();
        prog_->enableAttributeArray(1);
        prog_->setAttributeBuffer(1, GL_FLOAT, 0, 3);
    }

    if (vboTexture_) {
        vboTexture_->bind();
        prog_->enableAttributeArray(2);
        prog_->setAttributeBuffer(2, GL_FLOAT, 0, 2, 3 * sizeof(GLfloat));
    }

    // Cache Uniform Locations
    modelLoc_ = prog_->uniformLocation("model");
    Q_ASSERT(modelLoc_ >= 0);

    viewLoc_ = prog_->uniformLocation("view");
    Q_ASSERT(viewLoc_ >= 0);

    projectionLoc_ = prog_->uniformLocation("projection");
    Q_ASSERT(projectionLoc_ >= 0);

    // assign texture units
    if (diffuseMap_)
        prog_->setUniformValue("material.diffuse", 0);

    // Release (unbind) all
    prog_->release();

    if (diffuseMap_)
        diffuseMap_->release();

    vao_->release();
    ebo_->release();
    vbo_->release();

    if (vboNormal_)
        vboNormal_->release();

    if (vboTexture_)
        vboTexture_->release();

    translate(0.0f, -1.75f, 0.0f);
    scale(0.2f, 0.2f, 0.2f);
    return true;
}

void MeshProg::paint(const Camera &camera, const QMatrix4x4 &projection, const Light &light)
{
    bool rc = false;

    rc = prog_->bind();
    Q_ASSERT(rc);

    if (diffuseMap_)
        diffuseMap_->bind(0);

    prog_->setUniformValue(modelLoc_, toMatrix());
    prog_->setUniformValue(viewLoc_, camera.viewMatrix());
    prog_->setUniformValue(projectionLoc_, projection);
    prog_->setUniformValue(viewPosLoc_, camera.position);

    if (vboNormal_)
        prog_->setUniformValue("normal", toMatrix().normalMatrix());

    prog_->setUniformValue("material.ambient", material_.ambient);
    prog_->setUniformValue("material.diffuse", material_.diffuse);
    prog_->setUniformValue("material.specular", material_.specular);
    prog_->setUniformValue("material.shininess", material_.shininess);

    prog_->setUniformValue("light.position", light.position);
    prog_->setUniformValue("light.ambient", light.ambient);
    prog_->setUniformValue("light.diffuse", light.diffuse);
    prog_->setUniformValue("light.specular", light.specular);

    vao_->bind();
    glFunc_->glDrawElements(GL_TRIANGLES, numIndices_, GL_UNSIGNED_INT, nullptr);
    vao_->release();

    if (diffuseMap_)
        diffuseMap_->release();

    prog_->release();
}

void MeshProg::setMaterial(const Material& material)
{
    material_ = material;
}

void MeshProg::reset()
{
    if (diffuseMap_) {
        delete diffuseMap_;
        diffuseMap_ = nullptr;
    }

    if (specularMap_) {
        delete specularMap_;
        specularMap_ = nullptr;
    }

    if (vao_) {
        delete vao_;
        vao_ = nullptr;
    }

    if (ebo_) {
        delete ebo_;
        ebo_ = nullptr;
    }

    if (vboTexture_) {
        delete vboTexture_;
        vboTexture_ = nullptr;
    }

    if (vboNormal_) {
        delete vboNormal_;
        vboNormal_ = nullptr;
    }

    if (vbo_) {
        delete vbo_;
        vbo_ = nullptr;
    }

    if (prog_) {
        delete prog_;
        prog_ = nullptr;
    }

    if (vertexIndices_) {
        delete [] vertexIndices_;
        vertexIndices_ = nullptr;
    }

    numIndices_ = 0;

    modelLoc_ = viewLoc_ = projectionLoc_ = viewPosLoc_ = -1;
    glFunc_ = nullptr;
    mesh_ = nullptr;

    if (ctx_) {
        disconnect(ctx_, SIGNAL(aboutToBeDestroyed()), this, SLOT(reset()));
        ctx_ = nullptr;
    }
}

unsigned int* MeshProg::initializeVertexIndices(size_t& numIndices)
{
    Q_ASSERT(mesh_);
    Q_ASSERT(mesh_->mNumFaces);
    Q_ASSERT(mesh_->mFaces);

    numIndices = 0;
    size_t primitiveVertexCount = 0;

    switch (mesh_->mPrimitiveTypes) {

    case aiPrimitiveType_POINT:
        primitiveVertexCount = 1;
        break;

    case aiPrimitiveType_LINE:
        primitiveVertexCount = 2;
        break;

    case aiPrimitiveType_TRIANGLE:
        primitiveVertexCount = 3;
        break;

    default:
        break;
    }

    if (! primitiveVertexCount) {
        qDebug("%s: this %p, primitive type %u, numFaces %d", Q_FUNC_INFO, this, mesh_->mPrimitiveTypes);
        return nullptr;
    }

    numIndices = primitiveVertexCount * mesh_->mNumFaces;
    unsigned int* indices = new (nothrow) unsigned int [numIndices];

    for (unsigned int i = 0; i < mesh_->mNumFaces; ++i) {

        if ((mesh_->mFaces + i)->mNumIndices == primitiveVertexCount)
            memcpy(indices + i * primitiveVertexCount,
                   (mesh_->mFaces + i)->mIndices,
                   primitiveVertexCount * sizeof(unsigned int));
        else
            qWarning("%s: invalid face %d, in mesh %p", Q_FUNC_INFO, i, this);
    }

    return indices;
}
